#!/usr/bin/python

# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.

import imp
import os
from contextlib import redirect_stderr, redirect_stdout
from optparse import OptionParser

import lldb


def __lldb_init_module(debugger, dict):
    filePath = os.path.realpath(__file__)
    lldbHelperDir = os.path.dirname(filePath)

    commandsDirectory = os.path.join(lldbHelperDir, "commands")
    loadCommandsInDirectory(commandsDirectory)


def loadCommandsInDirectory(commandsDirectory):
    for file in os.listdir(commandsDirectory):
        fileName, fileExtension = os.path.splitext(file)
        if fileExtension == ".py":
            module = imp.load_source(fileName, os.path.join(commandsDirectory, file))

            if hasattr(module, "lldbinit"):
                module.lldbinit()

            if hasattr(module, "lldbcommands"):
                module._loadedFunctions = {}
                for command in module.lldbcommands():
                    loadCommand(
                        module, command, commandsDirectory, fileName, fileExtension
                    )


def loadCommand(module, command, directory, filename, extension):
    func = makeRunCommand(command, os.path.join(directory, filename + extension))
    name = command.name()
    helpText = (
        command.description().strip().splitlines()[0]
    )  # first line of description

    key = filename + "_" + name

    module._loadedFunctions[key] = func

    functionName = "__" + key

    lldb.debugger.HandleCommand(
        "script "
        + functionName
        + " = sys.modules['"
        + module.__name__
        + "']._loadedFunctions['"
        + key
        + "']"
    )
    lldb.debugger.HandleCommand(
        'command script add --help "{help}" --function {function} {name}'.format(
            help=helpText.replace('"', '\\"'),  # escape quotes
            function=functionName,
            name=name,
        )
    )


def makeRunCommand(command, filename):
    def runCommand(debugger, input, exe_ctx, result, _):
        # lldb assumes that any output meant for the user is written
        # to the result object. By redirecting stdout here, we can
        # use methods like print (or parse_args) in the command logic
        # as if they are writing to stdout, but write to result
        # instead. lldb will handle displaying it to the user.
        with redirect_stdout(result), redirect_stderr(result):
            command.result = result
            command.context = exe_ctx
            splitInput = command.lex(input)

            # OptionParser will throw in the case where you want just one
            # big long argument and no options and you enter something
            # that starts with '-' in the argument. e.g.:
            #     somecommand -[SomeClass someSelector:]
            # This solves that problem by prepending a '--' so that
            # OptionParser does the right thing.
            options = command.options()
            if len(options) == 0:
                if "--" not in splitInput:
                    splitInput.insert(0, "--")

            parser = optionParserForCommand(command)
            (options, args) = parser.parse_args(splitInput)

            # When there are more args than the command has declared, assume
            # the initial args form an expression and combine them into a single arg.
            if len(args) > len(command.args()):
                overhead = len(args) - len(command.args())
                head = args[: overhead + 1]  # Take N+1 and reduce to 1.
                args = [" ".join(head)] + args[-overhead:]

            if validateArgsForCommand(args, command):
                command.run(args, options)

    runCommand.__doc__ = helpForCommand(command, filename)
    return runCommand


def validateArgsForCommand(args, command):
    if len(args) < len(command.args()):
        defaultArgs = [arg.default for arg in command.args()]
        defaultArgsToAppend = defaultArgs[len(args) :]

        index = len(args)
        for defaultArg in defaultArgsToAppend:
            if not defaultArg:
                arg = command.args()[index]
                print("Whoops! You are missing the <" + arg.argName + "> argument.")
                print("\nUsage: " + usageForCommand(command))
                return
            index += 1

        args.extend(defaultArgsToAppend)
    return True


def optionParserForCommand(command):
    parser = OptionParser()

    for argument in command.options():
        if argument.boolean:
            parser.add_option(
                argument.shortName,
                argument.longName,
                dest=argument.argName,
                help=argument.help,
                action=("store_false" if argument.default else "store_true"),
            )
        else:
            parser.add_option(
                argument.shortName,
                argument.longName,
                dest=argument.argName,
                help=argument.help,
                default=argument.default,
            )

    return parser


def helpForCommand(command, filename):
    help = command.description()

    argSyntax = ""
    optionSyntax = ""

    if command.args():
        help += "\n\nArguments:"
        for arg in command.args():
            help += "\n  <" + arg.argName + ">; "
            if arg.argType:
                help += "Type: " + arg.argType + "; "
            help += arg.help
            argSyntax += " <" + arg.argName + ">"

    if command.options():
        help += "\n\nOptions:"
        for option in command.options():
            if option.longName and option.shortName:
                optionFlag = option.longName + "/" + option.shortName
            elif option.longName:
                optionFlag = option.longName
            else:
                optionFlag = option.shortName

            help += "\n  " + optionFlag + " "

            if not option.boolean:
                help += "<" + option.argName + ">; Type: " + option.argType

            help += "; " + option.help

            optionSyntax += " [{name}{arg}]".format(
                name=(option.longName or option.shortName),
                arg=("" if option.boolean else ("=" + option.argName)),
            )

    help += "\n\nSyntax: " + command.name() + optionSyntax + argSyntax

    help += "\n\nThis command is implemented as %s in %s." % (
        command.__class__.__name__,
        filename,
    )

    return help


def usageForCommand(command):
    usage = command.name()
    for arg in command.args():
        if arg.default:
            usage += " [" + arg.argName + "]"
        else:
            usage += " " + arg.argName

    return usage
